通过 Webpack 工具,可以很方便完成各种框架的构建打包支持。使用 Webpack 一段事件后,各种配置也都非常熟悉, 但只停留使用节点,对内部原理极致不是非常清新,常常带着这些疑问:
Webpack 启动流程是怎么样的?
Webpack 插件是怎么使用的,怎么保证调用顺序?
Webpack 事件机制是怎么样的?
接下来我将通过从 Webpack 启动流程, 事件机制, 插件机制, 热更新等几方面深入的讲述一下构建 Webpack 内部构建流程。
启动流程
首先我们来看看webpack的 webpack.js入口定义:
function webpack(options, callback) {
......
// 初始化所有plugin, 包括自定义事件, 事件回调定义
if(options.plugins && Array.isArray(options.plugins)) {
compiler.apply.apply(compiler, options.plugins);
}
var compiler = new Compiler();
// 这里比较关键,如果有提供回调函数,直接启动编译,这个是用于发布构建使用,
// 构建文件落地磁盘,需要提供callback进入构建流程;当使用 webpack-dev-middlerware
// 和 webpack-hot-middleware 时,不需要提供callback函数, 由 触发。
if(callback) {
// 启动 webpack 编译
compiler.run(callback);
}
return compiler;
}
以上图片来自冯淼森的博客
事件机制
关键事件
before-run
- NodeEnvironmentPlugin
run
- CachePlugin
watch-run
- CachePlugin
before-compile
compile
entry-option
- EntryOptionPlugin
make
- SingleEntryPlugin
after-compile
- CachePlugin
after-emit
- SizeLimitsPlugin
after-resolvers
AMDPlugin
NodeSourcePlugin
compilation
FunctionModulePlugin
NodeSourcePlugin
LoaderTargetPlugin
EvalSourceMapDevToolPlugin
CompatibilityPlugin
HarmonyModulesPlugin
AMDPlugin
CommonJsPlugin
LoaderPlugin
NodeStuffPlugin
RequireJsStuffPlugin
APIPlugin
ConstPlugin
UseStrictPlugin
RequireIncludePlugin
RequireEnsurePlugin
RequireContextPlugin
ImportPlugin
SystemPlugin
EnsureChunkConditionsPlugin
RemoveParentModulesPlugin
RemoveEmptyChunksPlugin
MergeDuplicateChunksPlugin
FlagIncludedChunksPlugin
OccurrenceOrderPlugin
FlagDependencyExportsPlugin
FlagDependencyUsagePlugin
TemplatedPathPlugin
RecordIdsPlugin
WarnCaseSensitiveModulesPlugin
this-compilation
CachePlugin
JsonpTemplatePlugin
done
上面是列举的几个重要的事件名,通过打日志发现,你还会发现还有很多自定义事件, 更多事件请参考官网Event Hooks。你可以通过 compiler.plugin(‘事件名’, callback) 的方式监听这些事件,并提供回调函数。通过Webpack构建提供的生命周期事件,你可以控制 Webpack 编译流程的每个环节,从而实现对 Webpack 的自定义扩展功能。
事件定义
- 全局事件容器定义
// Tapable.prototype.plugin 定义事件, 一个事件可以多个回调函数
compiler.plugin = function plugin(name, fn){
if(Array.isArray(name)) {
name.forEach(function(name) {
this.plugin(name, fn);
}, this);
return;
}
if(!this._plugins[name]) this._plugins[name] = [fn];
else this._plugins[name].push(fn);
}
- Webpack 启动入口组件初始化
// node_modules/webpack/lib/webpack.js
function webpack(options, callback) {
......
// 初始化所有plugin, 同时注册自定义事件和定义事件回调
if(options.plugins && Array.isArray(options.plugins)) {
// apply 是每个plugin必须实现的方法
compiler.apply.apply(compiler, options.plugins);
}
......
}
- 插件内部事件注册
查阅代码 Webpack 插件代码你会发现, 很多插件会在 apply
里面监听关键事件,然后处理相关逻辑
apply(compiler) {
compiler.plugin("compilation", (compilation, params) => {
......
});
compiler.plugin("make", (compilation, callback) => {
......
});
}
触发事件
在 node_modules/tapable/lib/Tapable.js
文件中提供很多触发事件的方法(方法命名好多,1,2,3,4这种命名,怀疑是版本兼容时不停增加导致的),下面介绍一下主要的两个。
- applyPlugins
compiler.applyPlugins("compile", params);
- applyPluginsAsync
compiler.applyPluginsAsync("before-compile", params, err => {
});
applyPluginsAsyncSeries [ Compiler {
_plugins:
{ ‘before-run’: [Array],
‘this-compilation’: [Array],
compilation: [Array],
‘after-resolvers’: [Array],
‘entry-option’: [Array],
make: [Array],
‘after-emit’: [Array],
‘watch-run’: [Array],
run: [Array],
‘after-compile’: [Array] },
代码执行流程
webpack.js
WebpackOptionsDefaulter 初始化 webpack 默认配置
NodeEnvironmentPlugin.apply(before-run)
初始化 inputFileSystem/outputFileSystem/watchFileSystem
compiler.applyPlugins(“environment”);
compiler.applyPlugins(“after-environment”);
WebpackOptionsApply
根据 webpack 配置 target 初始化 对应 Webpack plugin, 同时初始化文件查找
ResolverFactory.createResolver
web
compiler.apply(
// jsonp-script, require-ensure, bootstrap 脚本注入
new JsonpTemplatePlugin(options.output),
// __webpack_require__ 定义
new FunctionModulePlugin(options.output),
new NodeSourcePlugin(options.node),
new LoaderTargetPlugin(options.target)
);
node
compiler.apply(
new NodeTemplatePlugin({
asyncChunkLoading: options.target === "async-node"
}),
new FunctionModulePlugin(options.output),
new NodeTargetPlugin(),
new LoaderTargetPlugin("node")
);
compiler.apply(new EntryOptionPlugin());
compiler.applyPluginsBailResult("entry-option", options.context, options.entry);
compiler.apply(
new CompatibilityPlugin(),
new HarmonyModulesPlugin(options.module),
new AMDPlugin(options.module, options.amd || {}),
new CommonJsPlugin(options.module),
new LoaderPlugin(),
new NodeStuffPlugin(options.node),
new RequireJsStuffPlugin(),
new APIPlugin(),
new ConstPlugin(),
new UseStrictPlugin(),
new RequireIncludePlugin(),
new RequireEnsurePlugin(),
new RequireContextPlugin(options.resolve.modules, options.resolve.extensions, options.resolve.mainFiles),
new ImportPlugin(options.module),
new SystemPlugin(options.module)
);
compiler.apply(
new EnsureChunkConditionsPlugin(),
new RemoveParentModulesPlugin(),
new RemoveEmptyChunksPlugin(),
new MergeDuplicateChunksPlugin(),
new FlagIncludedChunksPlugin(),
new OccurrenceOrderPlugin(true),
new FlagDependencyExportsPlugin(),
new FlagDependencyUsagePlugin()
);
if(options.performance) {
compiler.apply(new SizeLimitsPlugin(options.performance));
}
compiler.apply(new TemplatedPathPlugin());
compiler.apply(new RecordIdsPlugin());
compiler.apply(new WarnCaseSensitiveModulesPlugin());
if(options.cache) {
let CachePlugin = require("./CachePlugin");
compiler.apply(new CachePlugin(typeof options.cache === "object" ? options.cache : null));
}
compiler.run(callback) 进入run流程
Compiler extends Tapable
compiler.run(callback) 进入编译流程
run(callback) {
const startTime = Date.now();
const onCompiled = (err, compilation) => {
//console.log('---run:onCompiled');
if(err) return callback(err);
if(this.applyPluginsBailResult("should-emit", compilation) === false) {
this.applyPlugins("done", stats);
return callback(null, stats);
}
this.emitAssets(compilation, err => {
if(err) return callback(err);
if(compilation.applyPluginsBailResult("need-additional-pass")) {
this.applyPlugins("done", stats);
this.applyPluginsAsync("additional-pass", err => {
if(err) return callback(err);
this.compile(onCompiled);
});
return;
}
this.emitRecords(err => {
if(err) return callback(err);
this.applyPlugins("done", stats);
return callback(null, stats);
});
});
};
this.applyPluginsAsync("before-run", this, err => {
if(err) return callback(err);
this.applyPluginsAsync("run", this, err => {
if(err) return callback(err);
//console.log('---applyPluginsAsync:run');
this.readRecords(err => {
if(err) return callback(err);
this.compile(onCompiled);
});
});
});
}
Webpack Loader 处理初始化 NormalModuleFactory
NormalModuleFactory: /node_modules/webpack/lib/NormalModuleFactory.js
createNormalModuleFactory() {
// /node_modules/webpack/lib/NormalModuleFactory.js
const normalModuleFactory = new NormalModuleFactory(this.options.context, this.resolvers, this.options.module || {});
this.applyPlugins("normal-module-factory", normalModuleFactory);
return normalModuleFactory;
}
createContextModuleFactory() {
const contextModuleFactory = new ContextModuleFactory(this.resolvers, this.inputFileSystem);
this.applyPlugins("context-module-factory", contextModuleFactory);
return contextModuleFactory;
}
newCompilationParams() {
const params = {
normalModuleFactory: this.createNormalModuleFactory(),
contextModuleFactory: this.createContextModuleFactory(),
compilationDependencies: []
};
return params;
}
compiler.compile(onCompiled) 进入编译流程
compile(callback) {
const params = this.newCompilationParams();
this.applyPluginsAsync("before-compile", params, err => {
if(err) return callback(err);
this.applyPlugins("compile", params);
const compilation = this.newCompilation(params);
this.applyPluginsParallel("make", compilation, err => {
if(err) return callback(err);
compilation.finish();
compilation.seal(err => {
if(err) return callback(err);
this.applyPluginsAsync("after-compile", compilation, err => {
if(err) return callback(err);
return callback(null, compilation);
});
});
});
});
}
Entry
entry-option:EntryOptionPlugin
make:SingleEntryPlugin
// Compilation: node_modules/webpack/lib/Compilation.js
Compilation.addEntry(context, entry, name, callback)
关键代码
Webpack.js
function webpack(options)
new WebpackOptionsDefaulter().process(options);
compiler.apply.apply(compiler, options.plugins);
new NodeEnvironmentPlugin().apply(compiler);
NodeEnvironmentPlugin.js: compiler.plugin(“before-run”)
compiler.applyPlugins(“environment”);
compiler.applyPlugins(“after-environment”);
compiler.options = new WebpackOptionsApply().process(options, compiler);
WebpackOptionsApply.js
EntryOptionPlugin: “entry-option”
SingleEntryPlugin: “make” or MultiEntryPlugin: “make”
若干组件初始化
compiler.resolvers.context = ResolverFactory.createResolver(options.resolve)
compiler.resolvers.loader = ResolverFactory.createResolver(options.resolveLoader);
compiler.run(callback)
Compiler.js
compiler.run(callback)
this.applyPluginsAsync(“before-run”)
this.applyPluginsAsync(“run”)
this.compile(onCompiled);
new NormalModuleFactory(this.options.context, this.resolvers, this.options.module || {})
this.applyPluginsAsync(“before-compile”)
this.applyPlugins(“compile”)
this.applyPluginsParallel(“make”)
this.applyPluginsAsync(“after-compile”)
callback(null, compilation)
WebpackOptionsApply.js
new EntryOptionPlugin: ‘entry-option’
compiler.apply(‘entry-option’)
compiler.apply(new SingleEntryPlugin: “make” or MultiEntryPlugin: “make”);
SingleEntryPlugin
- compiler.plugin(“make”, (compilation, callback) => {
const dep = SingleEntryPlugin.createDependency(this.entry, this.name);
compilation.addEntry(this.context, dep, this.name, callback);
- compiler.plugin(“make”, (compilation, callback) => {
});
若干组件初始化
compiler.resolvers.context = ResolverFactory.createResolver(options.resolve)
compiler.resolvers.loader = ResolverFactory.createResolver(options.resolveLoader);
Compilation.js
addEntry
_addModuleChain
NormalModuleFactory.create
buildModule:build-module
NormalModule.js: build
loader-runner:runLoaders
NormalModule.js: parser.parse HarmonyImportDependency 文件依赖
processModuleDependencies( 递归解析文件和处理文件依赖 )
Dependencies
- factory: NullFactory & NormalModuleFactory
[ HarmonyCompatibilityDependency { module: null, originModule: [Object], loc: [Object] } ],
[ HarmonyImportDependency {
module: null,
request: 'vue',
userRequest: 'vue',
range: [Array],
importedVar: '__WEBPACK_IMPORTED_MODULE_0_vue__',
loc: [Object] } ],
[ HarmonyImportDependency {
module: null,
request: './components/Hello.vue',
userRequest: './components/Hello.vue',
range: [Array],
importedVar: '__WEBPACK_IMPORTED_MODULE_1__components_Hello_vue__',
loc: [Object] } ],
[ HarmonyImportDependency {
module: null,
request: './components/HelloDecorator.vue',
userRequest: './components/HelloDecorator.vue',
range: [Array],
importedVar: '__WEBPACK_IMPORTED_MODULE_2__components_HelloDecorator_vue__',
loc: [Object] } ],
[ HarmonyImportSpecifierDependency {
module: null,
importDependency: [Object],
importedVar: '__WEBPACK_IMPORTED_MODULE_0_vue__',
id: 'default',
name: 'Vue',
range: [Array],
strictExportPresence: false,
namespaceObjectAsContext: false,
callArgs: undefined,
call: undefined,
directImport: true,
shorthand: undefined,
loc: [Object] } ],
[ HarmonyImportSpecifierDependency {
module: null,
importDependency: [Object],
importedVar: '__WEBPACK_IMPORTED_MODULE_1__components_Hello_vue__',
id: 'default',
name: 'HelloComponent',
range: [Array],
strictExportPresence: false,
namespaceObjectAsContext: false,
callArgs: undefined,
call: undefined,
directImport: true,
shorthand: undefined,
loc: [Object] } ],
[ HarmonyImportSpecifierDependency {
module: null,
importDependency: [Object],
importedVar: '__WEBPACK_IMPORTED_MODULE_2__components_HelloDecorator_vue__',
id: 'default',
name: 'HelloDecoratorComponent',
range: [Array],
strictExportPresence: false,
namespaceObjectAsContext: false,
callArgs: undefined,
call: undefined,
directImport: true,
shorthand: undefined,
loc: [Object] } ] ]
NormalModuleFactory.js
this.plugin(“factory”)
this.plugin(“resolver”)
create(data, callback)
创建模块:
new NormalModule(
result.request, ///TypeScript-Vue-Starter/node_modules/_ts-loader@3.2.0@ts-loader/index.js??ref--1!/TypeScript-Vue-Starter/src/index.ts
result.userRequest, //'/TypeScript-Vue-Starter/src/index.ts',
result.rawRequest, //'./src/index.ts'
result.loaders,
result.resource,
result.parser
);
https://github.com/webpack/enhanced-resolve/tree/master/lib
https://doc.webpack-china.org/concepts/module-resolution/
TypeScript-Vue-Starter/node_modules/enhanced-resolve/lib/ResolverFactory.js
TypeScript-Vue-Starter/node_modules/enhanced-resolve/lib/Resolver.js
TypeScript-Vue-Starter/node_modules/webpack/lib/NormalModuleFactory.js
ResolverFactory
- TypeScript-Vue-Starter/node_modules/_enhanced-resolve@3.4.1@enhanced-resolve/lib/ResolverFactory.js
- TypeScript-Vue-Starter/node_modules/enhanced-resolve/lib/node.js
- TypeScript-Vue-Starter/node_modules/webpack/lib/WebpackOptionsApply.js
- TypeScript-Vue-Starter/node_modules/_webpack@3.10.0@webpack/lib/webpack.js
new WebpackOptionsDefaulter().process(options);
compiler.options = new WebpackOptionsApply().process(options, compiler);
插件初始化
class Compiler extends Tapable
- 执行plugin 的apply方法
Tapable.prototype.apply = function apply() {
for(var i = 0; i < arguments.length; i++) {
console.log('Tapable#apply', arguments[i].constructor.name);
arguments[i].apply(this);
}
};
- 注册事件回调函数
// name hook 事件名称
// fn: function (request, callback) {
// resolver.doResolve(target, obj, appending, callback);
// }
Tapable.prototype.plugin = function plugin(name, fn) {
if(Array.isArray(name)) {
name.forEach(function(name) {
this.plugin(name, fn);
}, this);
return;
}
// 一个事件名可以注册多个回调函数
if(!this._plugins[name]) this._plugins[name] = [fn];
else this._plugins[name].push(fn);
};
插件执行循序
- SingleEntryPlugin